package com.iot.auth.service.impl;

import cn.hutool.core.lang.UUID;
import com.dc3.common.bean.R;
import com.dc3.common.constant.CacheConstant;
import com.iot.auth.pojo.Dc3User;
import com.iot.auth.service.UserService;
import com.iot.openfeign.CacheClient;
import com.iot.openfeign.TenantClient;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.relational.core.query.Criteria;
import org.springframework.data.relational.core.query.Query;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import javax.annotation.Resource;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.util.Date;
import java.util.HashMap;
import java.util.List;


/**
 * @Author 咕唧
 * @Date 2022/7/15 21:29
 * @Version 1.0
 */
@Service
@Transactional
public class UserServiceImpl implements UserService {

    private R2dbcEntityTemplate r2dbcEntityTemplate;

    private BCryptPasswordEncoder bCryptPasswordEncoder;

    private TenantClient tenantClient;


    private CacheClient cacheClient;

    @Resource
    public void setCacheClient(CacheClient cacheClient) {
        this.cacheClient = cacheClient;
    }

    @Resource
    public void setTenantClient(TenantClient tenantClient) {
        this.tenantClient = tenantClient;
    }

    @Autowired
    public void setbCryptPasswordEncoder(BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    @Autowired
    public void setR2dbcEntityTemplate(R2dbcEntityTemplate r2dbcEntityTemplate) {
        this.r2dbcEntityTemplate = r2dbcEntityTemplate;
    }

    @Override
    public Mono<Dc3User> findById(String id) {
        return r2dbcEntityTemplate.selectOne(Query.query(Criteria.where("id").is(id)), Dc3User.class);

    }

    @Override
    public Flux<Dc3User> findByIdIn(List<String> ids) {
        return Flux.just(ids)
                .flatMap(tenant -> r2dbcEntityTemplate.select(Dc3User.class).from("dc3_user")
                        .matching(Query.query(Criteria.where("id").in(ids)))
                        .all());
    }


    @Override
    public Mono<R<Object>> addUser(Mono<Dc3User> userMono, String tenantId) {

        return userMono
                .filter(dc3User -> StringUtils.hasText(dc3User.getName())
                        && StringUtils.hasText(dc3User.getPhone())
                        && StringUtils.hasText(dc3User.getPhone())
                        && StringUtils.hasText(dc3User.getEmail()))
                .flatMap(user1 -> {
                    //校验租户下是否存在该用户，根据用户名查询用户id，id不存在直接添加;
                    //id存在，则查询用户和租户的关联关系，没有关联关系，直接插入，有关联关系则不能直接插入
                    return findByUserId(user1.getName())
                            //根据用户id和租户id，通过openfeign远程调用查询是否存在关联关系
                            .flatMap(userIds -> {
                                return tenantClient.getCountByTentIdAndUserIds(userIds, tenantId)
                                        .filter(r -> ((Integer) r.getData()) == 0)
                                        .map(r -> user1);
                            });
                })
                .flatMap(dc3User -> {
                    dc3User.setId(UUID.randomUUID().toString(true));
                    dc3User.setPassword(bCryptPasswordEncoder.encode(dc3User.getPassword()));
                    return r2dbcEntityTemplate.insert(Dc3User.class).into("dc3_user").using(dc3User)
                            .flatMap(dc3User1 -> {
                                //把新增的用户添加到用户、租户绑定表当中
                                HashMap dc3TenantBindMap = new HashMap<>();
                                dc3TenantBindMap.put("tenantId", tenantId);
                                dc3TenantBindMap.put("userId", dc3User1.getId());
                                dc3TenantBindMap.put("description", "普通用户");
                                return tenantClient.bindUser2Tenant(dc3TenantBindMap);
                            });
                })
                .map(dc3User1 -> R.ok())
                .defaultIfEmpty(R.fail("添加失败，请检查数据是否准确"));

    }

    @Override
    public Flux<Dc3User> findByName(String name) {
        return r2dbcEntityTemplate.select(Dc3User.class).from("dc3_user")
                .matching(Query.query(Criteria.where("name").is(name))).all();
    }

    @Override
    public Mono<List<String>> findByUserId(String name) {
        return findByName(name).map(Dc3User::getId).collectList();
    }

    @Override
    public Mono<String> login(String username, String password, String tenantName) {
        //获取登录的用户名密码，查询数据库中是否存在该用户名，
        //根据数据库中的密码和用户传递的密码进行比较，相同，则登录成功，
        //生成一个变化的密码和jwt数据，缓存到redis中，
        //返回给用户，下次请求携带就可以直接登录成功
        // TODO 其它的判断,比如说A用户的手机号不用了(A不一定走了),B用户办理了这个手机号来这里上班了,结果分配的时候我们需要校验手机号.发现重复
        //TODO 如果同租户账号重复怎么办? 如果是开放注册就直接提示用户名存在,让注册的人换个账号,如果是非开放,由管理员添加的,管理员自行加个区别的内容就行了比如后面添加部门名字或者1 2 或者年月日等等
//        return findByNameAndTenantName(username, tenantName)
//                //过滤用户名和密码不相同的
//                .filter(dc3User -> bCryptPasswordEncoder.matches(password, dc3User.getPassword()))
//                //生成jwt数据返回给用户
//                .map(dc3User -> {
//                    //生成一个随机的盐，保存到redis中
//                    String salt = UUID.randomUUID().toString(true);
//                    //设置保存的key
//                    cacheClient.setValue(CacheConstant.Entity.TENANT + tenantName + CacheConstant.Entity.USER + username + CacheConstant.Suffix.SALT, salt).subscribe();
//                    return Jwts.builder()
//                            .setSubject(username)
//                            .claim("tenantName", tenantName)
//                            .claim("phone", dc3User.getPhone())
//                            .claim("email", dc3User.getEmail())
//                            .claim("description", dc3User.getDescription())
//                            //设置开始时间
//                            .setExpiration(Date.from(Instant.now().plusSeconds(2 * 60)))
//                            //设置过期时间
//                            .setIssuedAt(Date.from(Instant.now()))
//                            .signWith(SignatureAlgorithm.HS256, salt.getBytes(StandardCharsets.UTF_8))
//                            .compact();
//                    //TODO 还有一种方式,我们直接将jwt的字符串保存到redis中,并且根据有效期去设置有效期数据,当数据过期后会自动从redis中删除
//                    //校验的时候只需要比较传递过来的jwt的数据和redis里面的是不是一样,一样就代表没有篡改过,不一样就代表过期了
//
//                });
       return findByNameAndTenantName(username, tenantName)
                .filter(dc3User -> bCryptPasswordEncoder.matches(password, dc3User.getPassword()))
                .map(dc3User -> {
                    String salt = UUID.randomUUID().toString(true);
                    Date expiration = Date.from(Instant.now().plusSeconds(2 * 60));

                    String compact = Jwts.builder()
                            .setSubject(username)
                            .claim("tenantName", tenantName)
                            .claim("phone", dc3User.getPhone())
                            .claim("email", dc3User.getEmail())
                            .claim("description", dc3User.getDescription())
                            //设置开始时间
                            .setExpiration(expiration)
                            //设置过期时间
                            .setIssuedAt(Date.from(Instant.now()))
                            .signWith(SignatureAlgorithm.HS256, salt.getBytes(StandardCharsets.UTF_8))
                            .compact();
                    //设置带有过期时间的key
                    cacheClient.setValueWithExp(CacheConstant.Entity.TENANT + tenantName + CacheConstant.Entity.USER + username + CacheConstant.Suffix.SALT, compact,120L).subscribe();
                    return compact;
                });

    }

    @Override
    public Mono<Dc3User> findByNameAndTenantName(String username, String tenantName) {
        return r2dbcEntityTemplate
                .selectOne(Query.query(Criteria.where("name")
                        .is(username)
                        .and(Criteria.where("tenant_name").is(tenantName))), Dc3User.class);
    }
}
